/*
 * Copyright (C) 2009 Kristopher T Babic
 *
 * This program is free software; you can redistribute it and/or modify it under the terms of
 * the GNU General Public License as published by the Free Software Foundation; either
 * version 3 of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along with this program;
 * if not, see <http://www.gnu.org/licenses>.
 */

package edu.idp.client.anim;

import edu.idp.client.DrawType;
import edu.idp.client.Stopwatch;
import edu.idp.client.anim.sequence.Sequence;
import edu.idp.client.interfaces.SpriteAnimator;
import edu.idp.client.interfaces.Storable;
import edu.idp.shared.StoredObject;

import java.awt.*;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.Calendar;

/**
 * A sequence of images which are animated and moved about on
 * a Playfield.<p>
 *
 * @author Kristopher T Babic
 */
public class AdvancedSprite extends Component implements Cloneable, Storable {
    public static final long ANIM_TIME = 100;

    private SpriteAnimator animContainer;             // special container that contains the AdvancedSprite
    private Sequence currentSequence, mainSequence;   // sequences of images displayed by the AdvancedSprite
    private Stopwatch moveTimer = new Stopwatch();    // keeps track of when to change images

    private long moveInterval = 0;            // keeps track of the interval between images
    private long timeDifference = 0;            // used for time synchronization
    private long timeCreated = 0;            // when AdvancedSprite was created
    private Rectangle lastBounds = null;         // the boundary of the AdvancedSprite at its last location
    private long timeDeath = StoredObject.DEFAULT_DEATH; // the default time of death for the AdvancedSprite

    private AnimationPath path = null;                // the AnimationPath for the AdvancedSprite
    private boolean collisionDetect = true;           // whether or not to detect collisions with other AdvancedSprites

    static private final boolean _defaultCollisionState = true; // default for collision detection

    /**
     * Initializes an instance of an AdvancedSprite with the specified container and all
     * defaults all other conditions, such as path, location, etc.
     *
     * @param container the SpriteAnimator that contains the AdvancedSprite
     */
    public AdvancedSprite(SpriteAnimator container) {
        this.animContainer = container;
        this.setCreationTime(this.getTime());
    }

    /**
     * Initializes an instance of an AdvancedSprite with the specified container, sequence and
     * AnimationPath.
     *
     * @param container the SpriteAnimator that contains the AdvancedSprite
     * @param sequence  the sequence of images that will be used as the display of the AdvancedSprite
     * @param ap        the AnimationPath that will specify the location and movements of the AdvancedSprite
     */
    public AdvancedSprite(SpriteAnimator container,
                          Sequence sequence,
                          AnimationPath ap) {
        this(container, sequence, ap, _defaultCollisionState);
    }

    /**
     * Initializes an instance of an AdvancedSprite with the specified container, sequence, AnimationPath,
     * and time difference.
     *
     * @param container the SpriteAnimator that contains the AdvancedSprite
     * @param sequence  the sequence of images that will be used as the display of the AdvancedSprite
     * @param ap        the AnimationPath that will specify the location and movements of the AdvancedSprite
     * @param timeDiff  a long value used for time synchronization. this value is added to the current time
     */
    public AdvancedSprite(SpriteAnimator container,
                          Sequence sequence,
                          AnimationPath ap,
                          long timeDiff) {
        this(container, sequence, ap, _defaultCollisionState, timeDiff);
    }

    /**
     * Initializes an instance of an AdvancedSprite with the specified container, sequence, Animationpath and
     * collision detection state.
     *
     * @param container the SpriteAnimator that contains the AdvancedSprite
     * @param sequence  the sequence of images that will be used as the display of the AdvancedSprite
     * @param ap        the AnimationPath that will specify the location and movements of the AdvancedSprite
     * @param cState    if true, turn collision detection on, otherwise turn collision detection off
     */
    public AdvancedSprite(SpriteAnimator container,
                          Sequence sequence,
                          AnimationPath ap,
                          boolean cState) {
        this(container, sequence, ap, cState, 0l);
    }

    /**
     * Initializes an instance of an AdvancedSprite with the specified container, Sequence, AnimationPath,
     * collision detection state and time difference.
     *
     * @param container the SpriteAnimator that contains the AdvancedSprite
     * @param sequence  the sequence of images that will be used as the display of the AdvancedSprite
     * @param ap        the AnimationPath that will specify the location and movements of the AdvancedSprite
     * @param cState    if true, turn collision detection on, otherwise turn collision detection off
     * @param timeDiff  a long value used for time synchronization. this value is added to the current time
     */
    public AdvancedSprite(SpriteAnimator container,
                          Sequence sequence,
                          AnimationPath ap,
                          boolean cState,
                          long timeDiff) {

        // initialize all settings
        this.animContainer = container;
        sequence.setComponent((Component) container);
        setSequence(sequence);
        setMainSequence(sequence);

        // start sequence and timer
        moveTimer.start();
        currentSequence.start();

        // setup path and location
        setLocation(ap.getLocation());
        ap.reset();
        setPath(ap);

        setCollisions(cState);

        // setup time synchronization variables
        this.setTimeDifference(timeDiff);
        long t = this.getTime();
        this.setCreationTime(this.getTime());
    }

    /**
     * turns collision detection on and off
     *
     * @param state if true, turn collision detection on, else turn detection off
     */
    public void setCollisions(boolean state) {
        this.collisionDetect = state;
    }

    /**
     * returns the current collision detection state
     *
     * @return true if collision detection is on, false otherwise
     */
    public boolean getCollisions() {
        return this.collisionDetect;
    }

    /**
     * reverses the direction of the AdvancedSprite on the x-axis
     *
     * @deprecated the AnimationPath controls the movement of the sprite and that must
     *             be changed in order to change the direction of the AdvancedSprite
     */
    public void reverseX() {
    }

    /**
     * reverses the direction of the AdvancedSprite on the y-axis
     *
     * @deprecated the AnimationPath controls the movement of the sprite and that must
     *             be changed in order to change the direction of the AdvancedSprite
     */
    public void reverseY() {
    }

    /**
     * reverses the direction of the AdvancedSprite on the x-axis and y-axis
     *
     * @deprecated the AnimationPath controls the movement of the sprite and that must
     *             be changed in order to change the direction of the AdvancedSprite
     */
    public void reverse() {
    }

    /**
     * sets the current AnimationPath for the AdvancedSprite
     *
     * @param p the AnimationPath to be used to control the movement of the AdvancedSprite
     */
    public void setPath(AnimationPath p) {
        this.path = p;
    }

    /**
     * returns the current AnimationPath object being used to control the location of the AdvancedSprite
     *
     * @return the current AnimationPath being used to control the location of the AdvancedSprite
     */
    public AnimationPath getPath() {
        return this.path;
    }

    /**
     * draws the current image from the Sequence object to the graphics object being passed into
     * the paint method
     *
     * @param g the Graphics object to which the current image is being drawn too.
     */
    public void paint(Graphics g) {

        if (isVisible()) {
            Image image = currentSequence.getCurrentImage();
            try {
                g.drawImage(image, 0, 0, this);
            }
            catch (Exception e) {
            }
        }

    }

    /**
     * overwrites the parent update method so, the screen will not be cleared on every refresh
     */
    public void update(Graphics g) {
        paint(g);
    }

    /**
     * returns the preferred size of the AdvancedSprite.  This size is calculated by taking the
     * size of the current image from the Sequcnce object
     *
     * @return the dimension of the AdvancedSprite
     */
    public Dimension getPreferredSize() {

        Image image = currentSequence.getCurrentImage();
        return new Dimension(image.getWidth(this),
                             image.getHeight(this));

    }

    /**
     * uses the specified Sequence as the current sequence of images for the AdvancedSprite for
     * the specified number of cycles.  this in effect <i>plays</i> the sequence of images for
     * a short amount of time before going back to the original Sequence.
     */
    public void play(Sequence sequence, long cycles) {
        setSequence(sequence);
        sequence.setCyclesPerAnimation(cycles);
        sequence.setCurrentCycle(0);
    }

    /**
     * returns the time when the AdvancedSprite will no longer be displayed
     *
     * @return a long value specifying the AdvancedSprites time of death
     */
    protected long getDeathTime() {
        return ((getCreationTime() + getLifeSpan()) > 253401714000000l || (getCreationTime() + getLifeSpan()) < 0) ? 253401714000000l : getCreationTime() + getLifeSpan();
    }

    /**
     * checks to see if the AdvancedSprite is passed its death date.  If so, the AdvancedSprite is removed
     * from its container and destroyed.  Otherwise the AdvancedSprite checks to see if it has changed images
     * and/or location.  If the location or image has changed the AdvancedSprite is redisplayed to its container.
     */
    public void animate() {

        // check to see if the AdvancedSprite is still alive
        if (getTime() > getDeathTime()) {

            if (lastBounds != null)
                animContainer.redrawToForeground(lastBounds);

            Component c = this;
            Container myCont = null;

            // find the container containing the AdvancedSprite
            while ((c = c.getParent()) != null) {
                if (c instanceof Container) {
                    myCont = (Container) c;
                    break;
                }
            }

            // remove the AdvancedSprite from the container and destroy self
            if (myCont != null) {
                myCont.remove(this);
                animContainer.paintComponents(lastBounds.union(getBounds()), true);
                this.kill();
                return;
            }

        }

        // if current Sequence is finished, replace with main Sequence
        if (currentSequence.animationOver())
            currentSequence = mainSequence;

        // move/change image of AdvancedSprite and refresh
        if (timeToChangeImage()) currentSequence.advance();
        if (timeToMove()) {
            advance();
        }
        else {
            if (needsRepainting()) {
                animContainer.paintComponent(this);
            }
        }

    }

    /**
     * sets the main image Sequence of the AdvancedSprite
     *
     * @param sequence the sequence to use as the main image sequence for the AdvancedSprite
     */
    public void setMainSequence(Sequence sequence) {
        mainSequence = sequence;
    }

    /**
     * returns the current main image Sequence
     */
    public Sequence getMainSequence() {
        return mainSequence;
    }

    /**
     * sets the current image Sequence to the Sequence specified
     *
     * @param sequence the image Sequence to use as the current sequence
     */
    public void setSequence(Sequence sequence) {
        currentSequence = sequence;
    }

    /**
     * returns the image sequence currently being used by the AdvancedSprite
     */
    public Sequence getSequence() {
        return currentSequence;
    }

    /**
     * checks to see if the AdvancedSprite will intersect the specified AdvancedSprite on
     * the next movement of the AdvancedSprite
     */
    public boolean willIntersect(AdvancedSprite otherSprite) {
        return getNextBounds().intersects(
                otherSprite.getNextBounds());
    }

    /**
     * sets the center location of the AdvancedSprite to the x y location specified
     */
    public void setLocation(int x, int y) {

        lastBounds = this.getBounds();
        super.setLocation(x, y);

        // change the current location of of the AnimationPath
        if (path != null) {
            path.setLocation(new Point(x, y));
        }

    }

    /**
     * sets the time between movements of the Advancedsprite
     */
    public void setMoveInterval(long interval) {
        moveInterval = interval;
    }

    /**
     * sets the time between image changes
     */
    public void setImageChangeInterval(long interval) {
        currentSequence.setAdvanceInterval(interval);
    }

    /**
     * returns the next location of the AdvancedSprite.  currently it only
     * returns a location value from the AnimationPath.  this value is dependent on time
     * and so it makes it difficult to retrieve a future value.
     *
     * @return a Point containing the x and y coordinates of the next location for the AdvancedSprite
     */
    public Point getNextLocation() {
        return getPath().getLocation();
    }

    /**
     * returns the current location of the AdvancedSprite
     *
     * @return a Point object containing the x and y coordinates of the next location
     *         for the AdvancedSprite
     */
    public Point getLocation() {
        return getPath().getLocation();
    }

    /**
     * returns the next bounds of the AdvancedSprite component
     *
     * @return a Rectange object containing the x y coordinates, height and width of the next
     *         boundary for the AdvancedSprite component
     */
    public Rectangle getNextBounds() {
        Rectangle bounds = getBounds();
        Point nextLoc = getNextLocation();
        return new Rectangle(nextLoc.x - (bounds.width / 2),
                             nextLoc.y - (bounds.height / 2),
                             bounds.width, bounds.height);
    }

    /**
     * returns whether or not it is time to change the displayed image
     *
     * @returns true if it is time to change images, false otherwise
     */
    protected boolean timeToChangeImage() {
        return currentSequence.timeToAdvanceCell();
    }

    /**
     * returns whether or not it is time to change the AdvancedSprite's location
     *
     * @returns true if it is time to change locations, false otherwise
     */
    protected boolean timeToMove() {
        return moveTimer.elapsedTime() > moveInterval;
    }

    /**
     * returns whether or not the AdvancedSprite needs to be repainted
     */
    protected boolean needsRepainting() {
        Rectangle bounds = getBounds();
        return currentSequence.needsRepainting(
                new Point(bounds.x, bounds.y));
    }

    /**
     * changes images and location if needed to the next location/image
     */
    protected void advance() {
        Rectangle bounds = lastBounds;

        if (bounds == null)
            bounds = getBounds();

        animContainer.redrawToForeground(lastBounds);

        Image image = currentSequence.getCurrentImage();

        Point next = getPath().getLocation();

        // set new bounds for the AdvancedObject component
        setBounds(next.x - (image.getWidth(this) / 2),
                  next.y - (image.getHeight(this) / 2),
                  image.getWidth(this), image.getHeight(this));

        lastBounds = getBounds();

        // refresh the old and new locations on the screen
        animContainer.paintComponents(bounds.union(getBounds()), true);

    }

    /**
     * returns the InterDraw specifid DrawType for the AdvancedSprite
     */
    public int getType() {
        return DrawType.ANIMATED;
    }

    /**
     * returns a Component representation of the animation container containing the
     * AdvancedSprite
     */
    public Component getComponent() {
        return (Component) animContainer;
    }

    /**
     * restores the AdvancedSprite from the information contained within the
     * StoredObject passed into the method
     *
     * @param in a stored representation of an AdvancedSprite used to recreate
     *           the AdvancedSprite
     */
    public void restore(StoredObject in) {
        try {

            // retrieve the extra data from the StoredObject
            String out = in.getExtra();

            ObjectInputStream ois = null;
            ByteArrayInputStream bais = null;

            // retrieve dates
            this.setCreationTime(in.getDate());
            this.setLifeSpan(in.getDeath() - in.getDate());

            // uses ISO-8859-1 to make sure data isn't lost in conversion from String
            // to byte stream
            bais = new ByteArrayInputStream(out.getBytes("ISO-8859-1"));
            ois = new ObjectInputStream(bais);

            AnimationPath newPath = null;
            Sequence newSeq = null;

            // retrieve AnimationPath and image Sequence
            newPath = (AnimationPath) ois.readObject();
            newSeq = (Sequence) ois.readObject();

            // set the component
            newSeq.setComponent(getComponent());

            // set the time difference
            newPath.setTimeDifference(getTimeDifference());

            // initialize current location
            this.setPath(null);
            Point loc = newPath.getLocation();
            this.setLocation(loc.x, loc.y);
            this.setPath(newPath);

            this.setSequence(newSeq);

        }
        catch (Exception e) {
            e.printStackTrace();
            return;
        }

    }

    /**
     * creates and returns a StoredObject representation of the current AdvancedSprite<br>
     * <dd> object type
     * <dd> object colow
     * <dd> creation date
     * <dd> time of death
     * <dd> boundary
     * <dd> serialized image Sequence
     * <dd> serialized Animationpath
     */
    public StoredObject store() {

        StoredObject so = new StoredObject();
        so.setType(getType());
        so.setColor(null);
        so.setDate(this.getCreationTime());
        so.setDeath(getDeathTime());
        so.setBounds(null);

        try {
            AnimationPath ap = getPath();
            Sequence seq = getSequence();

            ObjectOutputStream oos = null;
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            oos = new ObjectOutputStream(baos);

            // write AnimationPath and Sequence as ByteStreams to StoredObject
            oos.writeObject(ap);
            Component c = seq.getComponent();
            oos.writeObject(seq);

            try {
                oos.close();
            }
            catch (Exception e) {
            }

            // uses ISO-8859-1 to make sure information isn't lost in conversion from
            // byte stream to String
            so.setExtra(new String(baos.toByteArray(), "ISO-8859-1"));
        }
        catch (Exception e) {
            e.printStackTrace();
            return null;
        }
        return so; //retStr;
    }

    /**
     * destroys the AdvancedSprite
     */
    public void kill() {
        moveTimer = null;
        try {
            this.finalize();
        }
        catch (Exception e) {
        }
        catch (Throwable t) {
        }
    }

    /**
     * Sets a new Sequence as the current Main Sequence
     *
     * @param s the sequence to set as the new main image Sequence
     */
    public void setNewSequence(Sequence s) {
        s.setComponent(getComponent());
        setSequence(s);
        setMainSequence(s);
        moveTimer.reset();
        currentSequence.start();
    }

    /**
     * returns a copy of this AdvancedSprite
     */
    public AdvancedSprite copy() {
        try {
            return (AdvancedSprite) this.clone();
        }
        catch (java.lang.Exception e) {
            return null;
        }
    }

    /**
     * returns a time synchronized version of the current time
     */
    protected long getTime() {
        return (Calendar.getInstance().getTime().getTime() + this.getTimeDifference());
    }

    /**
     * sets a time difference that is used in time synchronization
     */
    public void setTimeDifference(long time) {
        this.timeDifference = time;
        AnimationPath ap = getPath();
        ap.setTimeDifference(time);
        setPath(ap);
    }

    /**
     * returns the time difference that is used in time synchronization
     */
    public long getTimeDifference() {
        return this.timeDifference;
    }

    /**
     * sets the creation time of the AdvancedSprite
     */
    public void setCreationTime(long time) {
        this.timeCreated = time;
    }

    /**
     * returns the creation time fo the AdvancedSprite
     */
    public long getCreationTime() {
        return this.timeCreated;
    }

    /**
     * sets the total length of time the AdvancedSprite will exist
     */
    public void setLifeSpan(long time) {
        this.timeDeath = time;
    }

    /**
     * returns the total length of time the AdvancedSprite will exist
     */
    public long getLifeSpan() {
        return this.timeDeath;
    }

}
